// Android MNet runtime.
//
// Copyright 2011-2012 Martin Leidel, all rights reserved.
//
// This software is provided 'as-is', without any express or implied
// warranty.  In no event will the authors be held liable for any damages
// arising from the use of this software.
//
// Permission is granted to anyone to use this software for any purpose,
// including commercial applications, and to alter it and redistribute it
// freely, subject to the following restrictions:
//
// 1. The origin of this software must not be misrepresented; you must not
// claim that you wrote the original software. If you use this software
// in a product, an acknowledgment in the product documentation would be
// appreciated but is not required.
// 2. Altered source versions must be plainly marked as such, and must not be
// misrepresented as being the original software.
// 3. This notice may not be removed or altered from any source distribution.

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.HttpURLConnection;
import java.net.URL;
import java.net.URLEncoder;
import java.util.ArrayDeque;

import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothServerSocket;
import android.bluetooth.BluetoothSocket;

import org.json.*;
import java.math.BigInteger;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;

import java.math.BigInteger;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;

import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.ArrayList;

import java.net.URLConnection;

// *****************************************************************************
// *** HTTP stuff
// *****************************************************************************
class MNet_Http
{
  public static String _result = "";

  public static String GetResult()
  {
    return _result;
  }

  public String AGet( String url, int timeoutConnection, int timeoutSocket )
  {
    _result = "";
    httpThread ht = new httpThread( url, timeoutConnection, timeoutSocket );
    ht.start();
    return "started";
  }

  public String Get( String url, int timeoutConnection, int timeoutSocket )
  {
    HttpURLConnection con = null;
    InputStream is = null;
    try 
    {
      URL connectToURL = new URL( url );
      con = ( HttpURLConnection )connectToURL.openConnection();
      con.setReadTimeout( timeoutSocket );
      con.setConnectTimeout( timeoutConnection );
      con.setRequestMethod( "GET" );
      con.setDoInput( true );
      // Start the query
      con.connect();
      is = con.getInputStream();
    }
    catch( IOException e )
    {
      _result = "ERR_HttpGet " + e.getMessage();
    }
    return convertStreamToString( is );
  }

  public String convertStreamToString( InputStream is )
  {
    StringBuilder sb = new StringBuilder();
    try
    {
      BufferedReader reader = new BufferedReader( new InputStreamReader( is ) );
      String line = null;
      while( ( line = reader.readLine() ) != null )
      {
        sb.append( line + "\n" );
      }
      is.close();
    }
    catch( IOException e )
    {
      _result = "ERR_convertStreamToString " + e.getMessage();
    }
    return sb.toString();
  }
  
  private class httpThread extends Thread
  {
    String url;
    int timeoutConnection;
    int timeoutSocket;

    httpThread( String url, int timeoutConnection, int timeoutSocket )
    {
      this.url = url;
      this.timeoutConnection = timeoutConnection;
      this.timeoutSocket = timeoutSocket;
    }

    public void run()
    {
      try
      {
         _result = Get( url, timeoutConnection, timeoutSocket );
      }
      catch( Exception e )
      {
         _result = "ERR " + e.getMessage();
      }
    }
  }
}

// *****************************************************************************
// *** Pubnub stuff
// *****************************************************************************
class MNet_Pubnub
{
  public static Pubnub _pubnub = null;
  public static String _publishKey;
  public static String _subscribeKey;
  public static String _secretKey;
  public static int _pnInitSuccess = 0;
  public static pubnubThread _pnThread = null;

  public static void Init( String publishKey, String subscribeKey, String secretKey, int sslOn )
  {
    _publishKey = publishKey;
    _subscribeKey = subscribeKey;
    _secretKey = secretKey;
    _pnThread = null;
    boolean ssl = false;
    if( sslOn == 1 )
      ssl = true;
    
    _pubnub = new Pubnub( _publishKey, _subscribeKey, _secretKey, ssl );
    _pnInitSuccess = 1;
  }
  
  public void Subscribe( String channelID )
  {
    // Check if there is already a running thread
    if( _pnThread != null )
    {
      _pnThread.stop();
      _pnThread = null;
    }
    // Subscribe is a blocking call so we have to start a thread here!
    _pnThread = new pubnubThread( channelID );
    _pnThread.start();
  }
  
  public MNet_PubnubChannel GetChannel()
  {
    if( _pnThread != null )
      return _pnThread.getChannel();
    else
      return null;
  }
  
  private class pubnubThread extends Thread
  {
    String _channelID;
    MNet_PubnubChannel _pnChannel;
    
    pubnubThread( String channelID )
    {
      this._channelID = channelID;
      this._pnChannel = null;
    }

    public void run()
    {
      try
      {
         _pnChannel = new MNet_PubnubChannel( _channelID );
         _pnChannel.Subscribe();  // blocking call!
      }
      catch( Exception e )
      {
      }
    }
    
    public MNet_PubnubChannel getChannel()
    {
      return _pnChannel;
    }
  }
}

class MNet_PubnubChannel
{
  Receiver _receiver;
  String _channelID;
  Deque<String> _messages;
  Deque<String> _history;
  int _isConnected;
  int _isFetching;
  String _jsonKey;

  MNet_PubnubChannel( String id )
  {
    _channelID = id;
    _receiver = new Receiver();
    _messages = new ArrayDeque<String>();
    _history = new ArrayDeque<String>();
    _isConnected = 1;
    _isFetching = 0;
    _jsonKey = "MNetKey";
  }

  // blocking call!
  public void Subscribe()
  {
    MNet_Pubnub._pubnub.subscribe( _channelID, _receiver );
  }
  
  public void Send( String msg )
  {
    // Create JSON Message
    JSONObject message = new JSONObject();
    try
    {
      message.put( _jsonKey, msg );
    }
    catch ( org.json.JSONException jsonError )
    {
    }
    // Publish Message
    MNet_Pubnub._pubnub.publish(
      _channelID,    // Channel Name
      message        // JSON Message
    );
  }
  
  public int Connected()
  {
    return _isConnected;
  }
  
  public int MessageAvailable()
  {
    return _messages.size();
  }
  
  public String NextMessage()
  {
    if( _messages.size() > 0 )
      return _messages.pollFirst();
    else
      return "";
  }
  
  public void Fetch( int amount )
  {
    _isFetching = 1;
    // Get History
    JSONArray response = MNet_Pubnub._pubnub.history(
      _channelID,    // Channel Name
      amount         // Limit
    );
    _history.clear();
    try
    {
      for( int i = 0; i < response.length(); i++ )
      {
        JSONObject o = response.getJSONObject( i );
        _history.addLast( o.getString( _jsonKey ) );
      }
    }
    catch ( org.json.JSONException jsonError )
    {
    }
    _isFetching = 0;
  }
  
  public int Fetching()
  {
    return _isFetching;
  }
  
  public int FetchAvailable()
  {
    return _history.size();
  }
  
  public String NextFetch()
  {
    if( _history.size() > 0 )
      return _history.pollFirst();
    else
      return "";
  }
  
  public String ChannelID()
  {
    return _channelID;
  }

  class Receiver implements Callback
  {
    @Override
    public boolean execute( JSONObject message )
    {
      try
      {
        _messages.addLast( message.getString( _jsonKey ) );
      }
      catch ( org.json.JSONException jsonError )
      {
      }
      return true;
    }
  } 
}
  
  
  
  
  
  
  // *****************************************************************************
  // *** Bluetooth
  // *****************************************************************************
/*  
  private final BluetoothAdapter _btAdapter;
  private AcceptThread _secureAcceptThread;
  private AcceptThread _insecureAcceptThread;
  private ConnectThread _connectThread;
  private ConnectedThread _connectedThread;
  private int _state;

  private ArrayAdapter<String> _pairedDevicesArrayAdapter;
  private ArrayAdapter<String> _newDevicesArrayAdapter;

  // Constants that indicate the current connection state
  public static final int STATE_NONE = 0;       // we're doing nothing
  public static final int STATE_LISTEN = 1;     // now listening for incoming connections
  public static final int STATE_CONNECTING = 2; // now initiating an outgoing connection
  public static final int STATE_CONNECTED = 3;  // now connected to a remote device

  public static void btInit()
  {
    // Get the local Bluetooth adapter
    _btAdapter = BluetoothAdapter.getDefaultAdapter();
    _state = STATE_NONE;
  }
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
    // If there is none, Bluetooth is not supported
    if( _btAdapter == null )
    {
      // Device does not support Bluetooth
      return "Not supported!"
    }
    // Enable BT if not enabled
    if( !_btAdapter.isEnabled() )
    {
      Intent enableBtIntent = new Intent( BluetoothAdapter.ACTION_REQUEST_ENABLE );
      startActivityForResult( enableBtIntent, REQUEST_ENABLE_BT );
    }
    _pairedDevicesArrayAdapter = new ArrayAdapter<String>( this, R.layout.device_name );
    _newDevicesArrayAdapter = new ArrayAdapter<String>( this, R.layout.device_name );

    // Query paired devices (look for already known devices)
    Set<BluetoothDevice> pairedDevices = bluetoothAdapter.getBondedDevices();
    // If there are paired devices
    if( pairedDevices.size() > 0 )
    {
      // Loop through paired devices
      for( BluetoothDevice device : pairedDevices )
      {
        // Add the name and address to an array adapter to show in a ListView
        mArrayAdapter.add( device.getName() + "\n" + device.getAddress() );
      }
    }
  }
  */
  
  
  
/**
 * PubNub 3.0 Real-time Push Cloud API
 *
 * @author Stephen Blum
 * @package pubnub
 */
class Pubnub {
    private String ORIGIN        = "pubsub.pubnub.com";
    private int    LIMIT         = 1800;
    private String PUBLISH_KEY   = "";
    private String SUBSCRIBE_KEY = "";
    private String SECRET_KEY    = "";
    private boolean SSL          = false;

    /**
     * PubNub 3.0
     *
     * Prepare PubNub Class State.
     *
     * @param String Publish Key.
     * @param String Subscribe Key.
     * @param String Secret Key.
     * @param boolean SSL Enabled.
     */
    public Pubnub(
        String publish_key,
        String subscribe_key,
        String secret_key,
        boolean ssl_on
    ) {
        this.init( publish_key, subscribe_key, secret_key, ssl_on );
    }

    /**
     * PubNub 2.0 Compatibility
     *
     * Prepare PubNub Class State.
     *
     * @param String Publish Key.
     * @param String Subscribe Key.
     */
    public Pubnub(
        String publish_key,
        String subscribe_key
    ) {
        this.init( publish_key, subscribe_key, "", false );
    }

    /**
     * PubNub 3.0 without SSL
     *
     * Prepare PubNub Class State.
     *
     * @param String Publish Key.
     * @param String Subscribe Key.
     * @param String Secret Key.
     */
    public Pubnub(
        String publish_key,
        String subscribe_key,
        String secret_key
    ) {
        this.init( publish_key, subscribe_key, secret_key, false );
    }

    /**
     * Init
     *
     * Prepare PubNub Class State.
     *
     * @param String Publish Key.
     * @param String Subscribe Key.
     * @param String Secret Key.
     * @param boolean SSL Enabled.
     */
    public void init(
        String publish_key,
        String subscribe_key,
        String secret_key,
        boolean ssl_on
    ) {
        this.PUBLISH_KEY   = publish_key;
        this.SUBSCRIBE_KEY = subscribe_key;
        this.SECRET_KEY    = secret_key;
        this.SSL           = ssl_on;

        // SSL On?
        if (this.SSL) {
            this.ORIGIN = "https://" + this.ORIGIN;
        }
        else {
            this.ORIGIN = "http://" + this.ORIGIN;
        }
    }

    /**
     * Publish
     *
     * Send a message to a channel.
     *
     * @param String channel name.
     * @param JSONObject message.
     * @return boolean false on fail.
     */
    public JSONArray publish( String channel, JSONObject message ) {
        // Generate String to Sign
        String signature = "0";
        if (this.SECRET_KEY.length() > 0) {
            StringBuilder string_to_sign = new StringBuilder();
            string_to_sign
                .append(this.PUBLISH_KEY)
                .append('/')
                .append(this.SUBSCRIBE_KEY)
                .append('/')
                .append(this.SECRET_KEY)
                .append('/')
                .append(channel)
                .append('/')
                .append(message.toString());

            // Sign Message
            signature = md5(string_to_sign.toString());
        }

        // Build URL
        List<String> url = new ArrayList<String>();
        url.add("publish");
        url.add(this.PUBLISH_KEY);
        url.add(this.SUBSCRIBE_KEY);
        url.add(signature);
        url.add(channel);
        url.add("0");
        url.add(message.toString());

        // Return JSONArray
        return _request(url);
    }

    /**
     * Subscribe
     *
     * This function is BLOCKING.
     * Listen for a message on a channel.
     *
     * @param String channel name.
     * @param Callback function callback.
     */
    public void subscribe( String channel, Callback callback ) {
        this._subscribe( channel, callback, "0" );
    }

    /**
     * Subscribe - Private Interface
     *
     * Patch provided by petereddy on GitHub
     *
     * @param String channel name.
     * @param Callback function callback.
     * @param String timetoken.
     */
    private void _subscribe(
        String   channel,
        Callback callback,
        String   timetoken
    ) {
        while (true) {
            try {
                // Build URL
                List<String> url = java.util.Arrays.asList(
                    "subscribe", this.SUBSCRIBE_KEY, channel, "0", timetoken
                );

                // Wait for Message
                JSONArray response = _request(url);
                JSONArray messages = response.optJSONArray(0);

                // Update TimeToken
                if (response.optString(1).length() > 0)
                    timetoken = response.optString(1);

                // Run user Callback and Reconnect if user permits. If
                // there's a timeout then messages.length() == 0.
                for ( int i = 0; messages.length() > i; i++ ) {
                    JSONObject message = messages.optJSONObject(i);
                    if (!callback.execute(message)) return;
                }
            }
            catch (Exception e) {
                try { Thread.sleep(1000); }
                catch(InterruptedException ie) {}
            }
        }
    }


    /**
     * History
     *
     * Load history from a channel.
     *
     * @param String channel name.
     * @param int limit history count response.
     * @return JSONArray of history.
     */
    public JSONArray history( String channel, int limit ) {
        List<String> url = new ArrayList<String>();

        url.add("history");
        url.add(this.SUBSCRIBE_KEY);
        url.add(channel);
        url.add("0");
        url.add(Integer.toString(limit));

        return _request(url);
    }

    /**
     * Time
     *
     * Timestamp from PubNub Cloud.
     *
     * @return double timestamp.
     */
    public double time() {
        List<String> url = new ArrayList<String>();

        url.add("time");
        url.add("0");

        JSONArray response = _request(url);
        return response.optDouble(0);
    }

    /**
     * Request URL
     *
     * @param List<String> request of url directories.
     * @return JSONArray from JSON response.
     */
    private JSONArray _request(List<String> url_components) {
        String   json         = "";
        StringBuilder url     = new StringBuilder();
        Iterator url_iterator = url_components.iterator();

        url.append(this.ORIGIN);

        // Generate URL with UTF-8 Encoding
        while (url_iterator.hasNext()) {
            try {
                String url_bit = (String) url_iterator.next();
                url.append("/").append(_encodeURIcomponent(url_bit));
            }
            catch(Exception e) {
                e.printStackTrace();
                JSONArray jsono = new JSONArray();
                try { jsono.put("Failed UTF-8 Encoding URL."); }
                catch (Exception jsone) {}
                return jsono;
            }
        }

        // Fail if string too long
        if (url.length() > this.LIMIT) {
            JSONArray jsono = new JSONArray();
                try { 
                    jsono.put(0); 
                    jsono.put("Message Too Long."); 
                }
                catch (Exception jsone) {}
            return jsono;
        }

        try {
            URL            request = new URL(url.toString());
            URLConnection  conn    = request.openConnection();
            String         line    = "";

            conn.setConnectTimeout(200000);
            conn.setReadTimeout(200000);

            BufferedReader reader = new BufferedReader(
                new InputStreamReader(conn.getInputStream())
            );

            // Read JSON Message
            while ((line = reader.readLine()) != null) { json += line; }
            reader.close();

        } catch (Exception e) {

            JSONArray jsono = new JSONArray();

            try { jsono.put("Failed JSONP HTTP Request."); }
            catch (Exception jsone) {}

            e.printStackTrace();
            System.out.println(e);

            return jsono;
        }

        // Parse JSON String
        try { return new JSONArray(json); }
        catch (Exception e) {
            JSONArray jsono = new JSONArray();

            try { jsono.put("Failed JSON Parsing."); }
            catch (Exception jsone) {}

            e.printStackTrace();
            System.out.println(e);

            // Return Failure to Parse
            return jsono;
        }
    }

    private String _encodeURIcomponent(String s) {
        StringBuilder o = new StringBuilder();
        for (char ch : s.toCharArray()) {
            if (isUnsafe(ch)) {
                o.append('%');
                o.append(toHex(ch / 16));
                o.append(toHex(ch % 16));
            }
            else o.append(ch);
        }
        return o.toString();
    }

    private char toHex(int ch) {
        return (char)(ch < 10 ? '0' + ch : 'A' + ch - 10);
    }

    private boolean isUnsafe(char ch) {
        return " ~`!@#$%^&*()+=[]\\{}|;':\",./<>?".indexOf(ch) >= 0;
    }

    private String md5(String input) {
        try {
            MessageDigest md            = MessageDigest.getInstance("MD5");
            byte[]        messageDigest = md.digest(input.getBytes());
            BigInteger    number        = new BigInteger(1, messageDigest);
            String        hashtext      = number.toString(16);

            while (hashtext.length() < 32) hashtext = "0" + hashtext;

            return hashtext;
        }
        catch (NoSuchAlgorithmException e) {
            throw new RuntimeException(e);
        }
    }
}

class MD5 {
    public static String getMD5(String input) {
        try {
            MessageDigest md = MessageDigest.getInstance("MD5");
            byte[] messageDigest = md.digest(input.getBytes());
            BigInteger number = new BigInteger(1, messageDigest);
            String hashtext = number.toString(16);
            while (hashtext.length() < 32) {
                hashtext = "0" + hashtext;
            }
            return hashtext;
        }
        catch (NoSuchAlgorithmException e) {
            throw new RuntimeException(e);
        }
    }
}

interface Callback {
    public abstract boolean execute(JSONObject message);
}


